package com.baturu.mybatisminus.injector;

import com.baturu.mybatisminus.MinusConfiguration;
import com.baturu.mybatisminus.dao.LogicDeleteBaseDAO;
import com.baturu.mybatisminus.model.FieldInfo;
import com.baturu.mybatisminus.model.TableInfo;
import com.baturu.mybatisminus.enums.BaseDaoSQL;
import com.baturu.mybatisminus.util.EntityParseUtils;
import com.baturu.mybatisminus.util.ReflectionUtils;
import com.baturu.mybatisminus.util.SqlFragments;
import com.baturu.mybatisminus.util.StringUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.scripting.LanguageDriver;

import java.util.Collection;

/**
 * 默认的BaseDAO SQL注入类，目前仅支持mysql
 *
 * @author huangbin
 **/
public class DefaultBaseDaoSqlInjector implements BaseDaoSqlInjector {

    private static final Log logger = LogFactory.getLog(DefaultBaseDaoSqlInjector.class);

    protected MinusConfiguration configuration;
    protected LanguageDriver languageDriver;
    protected MapperBuilderAssistant builderAssistant;

    /**
     * 注入BaseDAO内置crud方法的SQL
     */
    @Override
    public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
        this.configuration = (MinusConfiguration) builderAssistant.getConfiguration();
        this.builderAssistant = builderAssistant;
        this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
        Class<?> entityClass = ReflectionUtils.extractEntityClass(mapperClass);
        Class<?> idClass = ReflectionUtils.extractIdClass(mapperClass);
        TableInfo tableInfo = EntityParseUtils.parseEntityClass(builderAssistant, entityClass, idClass);
        doInject(mapperClass, tableInfo);
    }

    protected void doInject(Class<?> mapperClass, TableInfo tableInfo) {
        injectSaveSql(mapperClass, tableInfo);
        injectDynamicSaveSql(mapperClass, tableInfo);
        injectBatchSaveSql(mapperClass, tableInfo);
        injectDeleteByIdSql(mapperClass, tableInfo);
        injectDeleteByIdsSql(mapperClass, tableInfo);
        injectLogicDeleteByIdSql(mapperClass, tableInfo);
        injectLogicDeleteByIdsSql(mapperClass, tableInfo);
        injectUpdateSql(mapperClass, tableInfo);
        injectDynamicUpdateSql(mapperClass, tableInfo);
        injectFindByIdSql(mapperClass, tableInfo);
        injectFindByIdsSql(mapperClass, tableInfo);
        injectFindAllSql(mapperClass, tableInfo);
        injectFindPageSql(mapperClass, tableInfo);
        injectCountSql(mapperClass, tableInfo);
        injectCustomizedSql(mapperClass, tableInfo);
    }


    protected void injectSaveSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.SAVE;
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                getInsertColumnString(tableInfo),
                getInsertParamString(tableInfo));
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getEntityClass());
        addInsertMappedStatement(tableInfo.getEntityClass(),
                getStatementId(mapperClass, baseDaoSQL.getMethodName()),
                sqlSource,
                tableInfo.getIdProperty(),
                tableInfo.getIdColumn());
    }


    protected void injectDynamicSaveSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.DYNAMIC_SAVE;
        StringBuilder columnBuilder = new StringBuilder("<trim suffix=\"\" suffixOverrides=\",\"> ");
        StringBuilder paramBuilder = new StringBuilder("<trim suffix=\"\" suffixOverrides=\",\"> ");
        for (FieldInfo fieldInfo : tableInfo.getInsertableFieldList()) {
            String condition;
            if (fieldInfo.isIgnoreEmptyString() && String.class.isAssignableFrom(fieldInfo.getPropertyType())) {
                condition = fieldInfo.getProperty() + " != null and " + fieldInfo.getProperty() + " !=\"\"";
            } else {
                condition = fieldInfo.getProperty() + " != null";
            }
            columnBuilder.append(SqlFragments.getIfFragment(condition, fieldInfo.getEscapedColumn() + ","));
            paramBuilder.append(SqlFragments.getIfFragment(condition, SqlFragments.getParam(fieldInfo.getProperty()) + ","));
        }
        columnBuilder.append("\n </trim>");
        paramBuilder.append("\n </trim>");
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                columnBuilder.toString(),
                paramBuilder.toString());
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getEntityClass());
        addInsertMappedStatement(tableInfo.getEntityClass(),
                getStatementId(mapperClass, baseDaoSQL.getMethodName()),
                sqlSource,
                tableInfo.getIdProperty(),
                tableInfo.getIdColumn());
    }


    protected void injectBatchSaveSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.BATCH_SAVE;
        StringBuilder paramBuilder = new StringBuilder();
        for (FieldInfo fieldInfo : tableInfo.getInsertableFieldList()) {
            paramBuilder.append("#{").append("item.").append(fieldInfo.getProperty()).append("}").append(",");
        }
        paramBuilder.setLength(paramBuilder.length() - 1);
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                getInsertColumnString(tableInfo),
                SqlFragments.getSaveForeachFragment(paramBuilder.toString()));
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Collection.class);
        addInsertMappedStatement(Collection.class,
                getStatementId(mapperClass, baseDaoSQL.getMethodName()),
                sqlSource,
                tableInfo.getIdProperty(),
                tableInfo.getIdColumn());
    }


    protected void injectDeleteByIdSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.DELETE_BY_ID;
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                tableInfo.getEscapedIdColumn(),
                SqlFragments.getParam(tableInfo.getIdColumn()));
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getIdClass());
        addDeleteMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, tableInfo.getIdClass());
    }


    protected void injectDeleteByIdsSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.DELETE_BY_IDS;
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                tableInfo.getEscapedIdColumn(),
                SqlFragments.getInForeachFragment());
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Collection.class);
        addDeleteMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, Collection.class);
    }

    protected void injectLogicDeleteByIdSql(Class mapperClass, TableInfo tableInfo) {
        String setString = getLogicDeleteStringIfNeedInject(mapperClass, tableInfo);
        if (StringUtils.isEmpty(setString)) {
            return;
        }
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.LOGIC_DELETE_BY_ID;
        String sql = String.format(baseDaoSQL.getSql(), tableInfo.getEscapedTableName(), setString,
                tableInfo.getEscapedIdColumn(), SqlFragments.getParam(tableInfo.getIdColumn()));
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getIdClass());
        addUpdateMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, tableInfo.getIdClass());
    }


    protected void injectLogicDeleteByIdsSql(Class mapperClass, TableInfo tableInfo) {
        String setString = getLogicDeleteStringIfNeedInject(mapperClass, tableInfo);
        if (StringUtils.isEmpty(setString)) {
            return;
        }
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.LOGIC_DELETE_BY_IDS;
        String sql = String.format(baseDaoSQL.getSql(), tableInfo.getEscapedTableName(), setString,
                tableInfo.getEscapedIdColumn(), SqlFragments.getInForeachFragment());
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Collection.class);
        addUpdateMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, Collection.class);
    }

    private String getLogicDeleteStringIfNeedInject(Class mapperClass, TableInfo tableInfo) {
        if (!LogicDeleteBaseDAO.class.isAssignableFrom(mapperClass)
                || (!tableInfo.containsProperty("state") && !tableInfo.containsProperty("status"))) {
            return null;
        }
        if (tableInfo.containsProperty("status")) {
            return "status = -1";
        } else {
            return "state = -1";
        }
    }


    protected void injectUpdateSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.UPDATE;
        StringBuilder setBuilder = new StringBuilder();
        for (FieldInfo fieldInfo : tableInfo.getUpdatableFieldList()) {
            setBuilder.append(fieldInfo.getEscapedColumn()).append("=").append(SqlFragments.getParam(fieldInfo.getProperty())).append(",");
        }
        setBuilder.setLength(setBuilder.length() - 1);
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                setBuilder.toString(),
                tableInfo.getEscapedIdColumn(),
                SqlFragments.getParam(tableInfo.getIdProperty()));
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getEntityClass());
        addUpdateMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, tableInfo.getEntityClass());
    }


    protected void injectDynamicUpdateSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.DYNAMIC_UPDATE;
        StringBuilder setBuilder = new StringBuilder("<set> \n");
        for (FieldInfo fieldInfo : tableInfo.getUpdatableFieldList()) {
            String condition;
            if (fieldInfo.isIgnoreEmptyString() && String.class.isAssignableFrom(fieldInfo.getPropertyType())) {
                condition = fieldInfo.getProperty() + " != null and " + fieldInfo.getProperty() + " !=\"\"";
            } else {
                condition = fieldInfo.getProperty() + " != null";
            }
            String content = fieldInfo.getEscapedColumn() + "=" + SqlFragments.getParam(fieldInfo.getProperty()) + ",";
            setBuilder.append(SqlFragments.getIfFragment(condition, content));
        }
        setBuilder.append("\n </set>");
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getEscapedTableName(),
                setBuilder.toString(),
                tableInfo.getEscapedIdColumn(),
                SqlFragments.getParam(tableInfo.getIdProperty()));
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getEntityClass());
        addUpdateMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, tableInfo.getEntityClass());
    }


    protected void injectFindByIdSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.FIND_BY_ID;
        String sql = String.format(baseDaoSQL.getSql(),
                tableInfo.getSelectColumnString(),
                tableInfo.getEscapedTableName(),
                tableInfo.getEscapedIdColumn(),
                tableInfo.getIdColumn());
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, tableInfo.getIdClass());
        addSelectMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, tableInfo.getIdClass(), tableInfo.getEntityClass());
    }


    protected void injectFindByIdsSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.FIND_BY_IDS;
        String sql = String.format(baseDaoSQL.getSql(), tableInfo.getSelectColumnString(), tableInfo.getEscapedTableName(), tableInfo.getEscapedIdColumn(), SqlFragments.getInForeachFragment());
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Collection.class);
        addSelectMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, Collection.class, tableInfo.getEntityClass());
    }


    protected void injectFindAllSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.FIND_ALL;
        String sql = String.format(baseDaoSQL.getSql(), tableInfo.getSelectColumnString(), tableInfo.getEscapedTableName());
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, null);
        addSelectMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, null, tableInfo.getEntityClass());
    }


    protected void injectFindPageSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.FIND_PAGE;
        String limit = SqlFragments.getIfFragment("param1 != null", "${param1.limitString}");
        String orderBy = SqlFragments.getIfFragment("param2 != null and param2.orders.size()>0", "${param2.orderBy}");
        String sql = String.format(baseDaoSQL.getSql(), tableInfo.getSelectColumnString(), tableInfo.getEscapedTableName(), orderBy + limit);
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, MapperMethod.ParamMap.class);
        addSelectMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, MapperMethod.ParamMap.class, tableInfo.getEntityClass());
    }


    protected void injectCountSql(Class mapperClass, TableInfo tableInfo) {
        BaseDaoSQL baseDaoSQL = BaseDaoSQL.COUNT;
        String sql = String.format(baseDaoSQL.getSql(), tableInfo.getEscapedTableName());
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, null);
        showSql(mapperClass, baseDaoSQL.getMethodName(), sql);
        addSelectMappedStatement(getStatementId(mapperClass, baseDaoSQL.getMethodName()), sqlSource, null, Integer.class);

    }

    protected void injectCustomizedSql(Class mapperClass, TableInfo tableInfo) {

    }


    /**
     * 查询
     */
    public MappedStatement addSelectMappedStatement(String statementId, SqlSource sqlSource, Class<?> parameterType, Class<?> resultType) {
        return this.addMappedStatement(statementId, sqlSource, SqlCommandType.SELECT, parameterType, null, resultType,
                new NoKeyGenerator(), null, null);
    }


    /**
     * 插入
     */
    public MappedStatement addInsertMappedStatement(Class<?> parameterClass, String statementId, SqlSource sqlSource, String keyProperty, String keyColumn) {
        return this.addMappedStatement(statementId, sqlSource, SqlCommandType.INSERT, parameterClass, null, Integer.class,
                new Jdbc3KeyGenerator(), keyProperty, keyColumn);
    }

    /**
     * 删除
     */
    public MappedStatement addDeleteMappedStatement(String statementId, SqlSource sqlSource, Class<?> parameterType) {
        return this.addMappedStatement(statementId, sqlSource, SqlCommandType.DELETE, parameterType, null, Integer.class,
                new NoKeyGenerator(), null, null);
    }

    /**
     * 更新
     */
    public MappedStatement addUpdateMappedStatement(String statementId, SqlSource sqlSource, Class<?> parameterType) {
        return this.addMappedStatement(statementId, sqlSource, SqlCommandType.UPDATE, parameterType, null, Integer.class,
                new NoKeyGenerator(), null, null);
    }


    public MappedStatement addMappedStatement(String statementId, SqlSource sqlSource,
                                              SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType,
                                              KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
        if (hasMappedStatement(statementId)) {
            logger.warn(statementId + " has been loaded");
            return null;
        }
        boolean isSelect = false;
        if (sqlCommandType == SqlCommandType.SELECT) {
            isSelect = true;
        }
        return builderAssistant.addMappedStatement(statementId, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null,
                parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
                configuration.getDatabaseId(), languageDriver, null);
    }


    private boolean hasMappedStatement(String mappedStatement) {
        return configuration.hasStatement(mappedStatement, false);
    }


    private void showSql(Class mapperClass, String methodName, String sql) {
        if (logger.isDebugEnabled() || configuration.isShowGeneratedSql()) {
            logger.debug(mapperClass.getSimpleName() + "." + methodName + " 生成 sql:\n" + sql);
        }
    }


    private String getStatementId(Class mapperClass, String methodName) {
        return mapperClass.getName() + "." + methodName;
    }

    private String getInsertColumnString(TableInfo tableInfo) {
        StringBuilder sb = new StringBuilder();
        for (FieldInfo fieldInfo : tableInfo.getInsertableFieldList()) {
            sb.append(fieldInfo.getEscapedColumn()).append(",");
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }

    public String getInsertParamString(TableInfo tableInfo) {
        StringBuilder sb = new StringBuilder();
        for (FieldInfo fieldInfo : tableInfo.getInsertableFieldList()) {
            sb.append(SqlFragments.getParam(fieldInfo.getProperty())).append(",");
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }


}
